module RCAP
  module Base
    class Area
      include Validation

      # @return [String] Textual description of area.
      attr_accessor(:area_desc)
      # @return [Numeric] Expressed in feet above sea level
      attr_accessor(:altitude)
      # @return [Numeric] Expressed in feet above sea level.
      attr_accessor(:ceiling)
      # @return [Array<Circle>]
      attr_reader(:circles)
      # @return [Array<Geocode>]
      attr_reader(:geocodes)
      # @return [Array<Polygon>]
      attr_reader(:polygons)

      validates_presence_of(:area_desc)
      validates_collection_of(:circles, :geocodes, :polygons, allow_empty: true)
      validates_dependency_of(:ceiling, on: :altitude)

      XML_ELEMENT_NAME       = 'area'
      AREA_DESC_ELEMENT_NAME = 'areaDesc'
      ALTITUDE_ELEMENT_NAME  = 'altitude'
      CEILING_ELEMENT_NAME   = 'ceiling'

      XPATH           = "cap:#{ XML_ELEMENT_NAME }"
      AREA_DESC_XPATH = "cap:#{ AREA_DESC_ELEMENT_NAME }"
      ALTITUDE_XPATH  = "cap:#{ ALTITUDE_ELEMENT_NAME }"
      CEILING_XPATH   = "cap:#{ CEILING_ELEMENT_NAME }"

      # @example
      #   Area.new do |area|
      #     area.area_desc = 'Cape Town CVD'
      #   end
      def initialize
        @area_desc = nil
        @altitude  = nil
        @ceiling   = nil

        @circles  = []
        @geocodes = []
        @polygons = []
        yield(self) if block_given?
      end

      # Creates a new {Polygon} object and adds it to the {#polygons} array.
      #
      # @return [Polygon]
      def add_polygon
        polygon_class.new.tap do |polygon|
          yield(polygon) if block_given?
          @polygons << polygon
        end
      end

      # Creates a new {Circle} object and adds it to the {#circles} array.
      #
      # @return [Circle]
      def add_circle
        circle_class.new.tap do |circle|
          yield(circle) if block_given?
          @circles << circle
        end
      end

      # Creates a new {Geocode} object and adds it to the {#geocodes} array.
      #
      # @return [Geocode]
      def add_geocode
        geocode_class.new do |geocode|
          yield(geocode) if block_given?
          @geocodes << geocode
        end
      end

      def self.from_xml_element(area_xml_element)
        new do |area|
          area.area_desc = RCAP.xpath_text(area_xml_element, AREA_DESC_XPATH, area.xmlns)
          area.altitude  = RCAP.to_f_if_given(RCAP.xpath_text(area_xml_element, ALTITUDE_XPATH, area.xmlns))
          area.ceiling   = RCAP.to_f_if_given(RCAP.xpath_text(area_xml_element, CEILING_XPATH, area.xmlns))

          RCAP.xpath_match(area_xml_element, area.circle_class::XPATH, area.xmlns).each do |circle_element|
            area.circles << area.circle_class.from_xml_element(circle_element)
          end

          RCAP.xpath_match(area_xml_element, area.geocode_class::XPATH, area.xmlns).each do |geocode_element|
            area.geocodes  << area.geocode_class.from_xml_element(geocode_element)
          end

          RCAP.xpath_match(area_xml_element, area.polygon_class::XPATH, area.xmlns).each do |polygon_element|
            area.polygons  << area.polygon_class.from_xml_element(polygon_element)
          end
        end
      end

      # @return [REXML::Element]
      def to_xml_element
        REXML::Element.new(XML_ELEMENT_NAME).tap do |xml_element|
          xml_element.add_element(AREA_DESC_ELEMENT_NAME).add_text(@area_desc.to_s)
          add_to_xml_element = lambda do |element, object|
            element.add_element(object.to_xml_element)
            element
          end
          @polygons.inject(xml_element, &add_to_xml_element)
          @circles.inject(xml_element, &add_to_xml_element)
          @geocodes.inject(xml_element, &add_to_xml_element)
          xml_element.add_element(ALTITUDE_ELEMENT_NAME).add_text(@altitude.to_s) unless @altitude.blank?
          xml_element.add_element(CEILING_ELEMENT_NAME).add_text(@ceiling.to_s)   unless @altitude.blank?
        end
      end

      # @return [String] XML representation of the Area
      def to_xml
        to_xml_element.to_s
      end

      # Implements an equality operator for the Area object. Two Area objects are equal if all their attributes are equal.
      #
      # @param [Area] other Area object to compare
      # @return [true,false]
      def ==(other)
        comparison_attributes = lambda { |area| [area.area_desc, area.altitude, area.ceiling, area.circles, area.geocodes, area.polygons] }
        comparison_attributes.call(self) == comparison_attributes.call(other)
      end

      # @return [String]
      def inspect
        area_inspect = "Area Description: #{ @area_desc }\n"\
                       "Polygons:\n" +                        @polygons.map { |polygon| '  ' + polygon.inspect }.join("\n") + "\n"\
                       "Circles:          #{ @circles.inspect }\n"\
                       "Geocodes:         #{ @geocodes.inspect }\n"
        RCAP.format_lines_for_inspect('AREA', area_inspect)
      end

      # Returns the area description
      #
      # @return [String]
      def to_s
        @area_desc
      end

      AREA_DESC_YAML = 'Area Description'
      ALTITUDE_YAML  = 'Altitude'
      CEILING_YAML   = 'Ceiling'
      CIRCLES_YAML   = 'Circles'
      GEOCODES_YAML  = 'Geocodes'
      POLYGONS_YAML  = 'Polygons'

      # @return [Hash]
      def to_yaml_data
        RCAP.attribute_values_to_hash([AREA_DESC_YAML, @area_desc],
                                      [ALTITUDE_YAML,  @altitude],
                                      [CEILING_YAML,   @ceiling],
                                      [CIRCLES_YAML,   @circles.map { |circle| circle.to_a }],
                                      [GEOCODES_YAML,  @geocodes.reduce({}) { |h, geocode| h.merge(geocode.name => geocode.value) }],
                                      [POLYGONS_YAML,  @polygons.map(&:to_yaml_data)])
      end

      # @return [String] YAML representation of object
      def to_yaml(options = {})
        to_yaml_data.to_yaml(options)
      end

      # @param [Hash] area_yaml_data
      # @return [Area]
      def self.from_yaml_data(area_yaml_data)
        new do |area|
          area.area_desc = area_yaml_data[AREA_DESC_YAML]
          area.altitude  = area_yaml_data[ALTITUDE_YAML]
          area.ceiling   = area_yaml_data[CEILING_YAML]

          Array(area_yaml_data[CIRCLES_YAML]).each do |circle_yaml_data|
            area.circles << area.circle_class.from_yaml_data(circle_yaml_data)
          end

          Array(area_yaml_data[GEOCODES_YAML]).each do |name, value|
            area.add_geocode do |geocode|
              geocode.name = name
              geocode.value = value
            end
          end

          Array(area_yaml_data[POLYGONS_YAML]).each do |polyon_yaml_data|
            area.polygons << area.polygon_class.from_yaml_data(polyon_yaml_data)
          end
        end
      end

      AREA_DESC_KEY = 'area_desc'
      ALTITUDE_KEY  = 'altitude'
      CEILING_KEY   = 'ceiling'
      CIRCLES_KEY   = 'circles'
      GEOCODES_KEY  = 'geocodes'
      POLYGONS_KEY  = 'polygons'

      # @param [Hash] area_hash
      # @return [Area]
      def self.from_h(area_hash)
        new do |area|
          area.area_desc = area_hash[AREA_DESC_KEY]
          area.altitude  = area_hash[ALTITUDE_KEY]
          area.ceiling   = area_hash[CEILING_KEY]

          Array(area_hash[CIRCLES_KEY]).each do |circle_array|
            area.circles << area.circle_class.from_a(circle_array)
          end

          Array(area_hash[GEOCODES_KEY]).each do |geocode_hash|
            area.geocodes << area.geocode_class.from_h(geocode_hash)
          end

          Array(area_hash[POLYGONS_KEY]).each do |polygon_hash|
            area.polygons << area.polygon_class.from_h(polygon_hash)
          end
        end
      end

      # @return [Hash]
      def to_h
        RCAP.attribute_values_to_hash([AREA_DESC_KEY, @area_desc],
                                      [ALTITUDE_KEY,  @altitude],
                                      [CEILING_KEY,   @ceiling],
                                      [CIRCLES_KEY,   @circles.map { |circle| circle.to_a }],
                                      [GEOCODES_KEY,  @geocodes.map { |geocode| geocode.to_h }],
                                      [POLYGONS_KEY,  @polygons.map { |polygon| polygon.to_h }])
      end
    end
  end
end
